package com.cd.university.config.security.impl;

import com.cd.university.common.utils.AesUtil;
import com.cd.university.config.security.authentication.util.SecurityThreadLocal;
import com.cd.university.config.security.authentication.util.ThreadLocalUser;
import com.cd.university.config.security.validate.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;

import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;

/**
 * 抽象的验证码校验,生成类
 * 具体的验证码校验只要继承这个抽象类即可
 * @author 陈栋
 * @create 2021/9/4 11:28
 */
public abstract class AbstractValidateCodeProcessor<C extends ValidateCode> implements ValidateCodeProcessor {

    /**
     * 这是spring的依赖查找
     * 如果声明了以下的配置，那么spring就会查找实现了ValidateCodeController的所有bean
     * 第一个参数String就是bean的名字
     */
    @Autowired
    private Map<String , ValidateCodeGenerator> validateCodeGenerators;

    /**
     * 验证代码存储库（可以表示存储在多种仓库中比如：redis和session中）
     */
    @Autowired
    private ValidateCodeRepository validateCodeRepository;

    @Autowired
    private AuthenticationFailureHandler cdAuthenticationFailureHandler;


    /**
     * 主要的效验码逻辑
     * 1.生成
     * 2.保存
     * 3.发送
     * @param request ServletWebRequest是Spring的一个封装类，里面封装了请求（request）和响应（response）
     * @throws Exception
     */
    @Override
    public void create(ServletWebRequest request) throws Exception {
        C validateCode = generate(request);
        save(request, validateCode);
        send(request, validateCode);
    }

    /**
     * 生成效验码
     * @param request 请求（request）和响应（response）
     * @return 返回一个验证码的实体类
     */
    @SuppressWarnings("unchecked")
    private C generate(ServletWebRequest request) {
        // 拿到具体的实例验证码类的前缀名
        String type = getValidateCodeType(request).toString().toLowerCase();
        // 拼接成具体的ValidateCodeGenerator类如imageValidateCodeGenerator
        String codeGeneratorName = type + ValidateCodeGenerator.class.getSimpleName();
        ValidateCodeGenerator validateCodeGenerator = validateCodeGenerators.get(codeGeneratorName);

        if (validateCodeGenerator==null) {
            throw new ValidateCodeException("验证码生成器：" + codeGeneratorName + "不存在");
        }

        // 调用具体类的生成验证码
        return (C) validateCodeGenerator.generate(request);
    }


    /**
     * 保存验证码
     * @param request 请求（request）和响应（response）
     * @param validateCode 验证码实例
     */
    private void save(ServletWebRequest request, C validateCode) {
        ValidateCode code = new ValidateCode(validateCode.getCode(), validateCode.getExpireTime());
        validateCodeRepository.save(request, code, getValidateCodeType(request));
    }

    /**
     * 发送验证码，因为短信发送验证码可能由不同的服务器厂商发送，所以需要一个抽象方法
     * @param request 请求（request）和响应（response）
     * @param validateCode 验证码实例
     */
    public abstract void send(ServletWebRequest request, C validateCode) throws Exception;

    /**
     * 验证验证码是否正确
     * 就是用缓存里面的验证码和前端传过来的验证码进行验证
     * @param servletWebRequest 请求和响应
     */
    @Override
    public void validate(ServletWebRequest servletWebRequest) {
        // 根据具体实例类拿到枚举类型
        ValidateCodeType codeType = getValidateCodeType(servletWebRequest);

        // 从本地存储的仓库（Session或者Redis）中拿验证码
        ValidateCode codeInRepo = validateCodeRepository.get(servletWebRequest, codeType);

        // 从前端拿到code
        String codeInRequest;
        // 尝试从前端传过来的json中拿数据
        codeInRequest = getCodeInRequest(servletWebRequest.getRequest(), servletWebRequest.getResponse(), codeType);

        if (codeInRequest==null) {
            try {
                codeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(),
                        codeType.getParamNameOnValidate());
            }catch (ServletRequestBindingException e) {
                new ValidateCodeException(codeType.getParamNameOnValidate() + "验证码不存在");
            }
        }

        if (StringUtils.isBlank(codeInRequest)) {
            throw new ValidateCodeException(codeType + "验证码的值不能为空");
        }

        if (codeInRepo == null) {
            throw new ValidateCodeException(codeType + "验证码不存在");
        }

        if (codeInRepo.isExpried()) {
            throw new ValidateCodeException(codeType + "验证码已过期");
        }

        if (!StringUtils.equals(codeInRepo.getCode(), codeInRequest)) {
            throw new ValidateCodeException(codeType + "验证码不匹配");
        }

        validateCodeRepository.remove(servletWebRequest, codeType);
    }

    /**
     * 根据具体的实现类拿到相应的ValidateCodeType
     * @param request 响应和请求
     * @return 验证码的枚举类型
     */
    private ValidateCodeType getValidateCodeType(ServletWebRequest request) {
        // 这是一个抽象类会有一个具体的实现类调用这个方法这时拿到的getClass就是具体实现类的Class
        String type = StringUtils.substringBefore(getClass().getSimpleName(), "ValidateCodeProcessor");
        System.out.println("AbstractValidateCodeProcessor类中获得的实现类："+getClass().getSimpleName());
        return ValidateCodeType.valueOf(type.toUpperCase());
    }

    /**
     * 先从json中拿验证码，如果没有再尝试从request中拿
     * @param request 前端请求参数
     * @return 验证码
     */
    private String getCodeInRequest(HttpServletRequest request, HttpServletResponse response, ValidateCodeType codeType) {
        String codeInRequest = null;
        if (request.getContentType().contains(MediaType.APPLICATION_JSON_VALUE)) {
            // 通过流解析出json 但是get delete就没有流
            try {
                ByteArrayOutputStream baos = cloneInputStream(request.getInputStream());

                Map<String ,String> map = new ObjectMapper().readValue(new ByteArrayInputStream(baos.toByteArray()), Map.class);
//                Map<String ,String> map = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                codeInRequest = map.get(codeType.getParamNameOnValidate());

                /*
                因为security中的表单登陆是通过form-data进行登录的，所以从json中拿不到数据，
                因此应该从json中取出，放在request中,方便后面的程序取出
                 */
                ThreadLocalUser localUser = new ThreadLocalUser();
                if (map.get("username")!=null) {
                    String username = map.get("username");
                    // 对用户名进行解密
                    try {
                        username = AesUtil.decrypt(username);
                    } catch (Exception e) {
                        // 调用登陆失败的接口
                        try {
                            cdAuthenticationFailureHandler.onAuthenticationFailure(request,response,new UsernameNotFoundException("用户名解密出错"));
                        } catch (ServletException servletException) {
                            servletException.printStackTrace();
                        }
                        throw new UsernameNotFoundException("用户名解密出错");
                    }
                    localUser.setUsername(username);
                }
                if (map.get("password")!=null) {
                    String password = map.get("password");

                    // 对密码进行解密
                    try {
                        password = AesUtil.decrypt(password);
                    } catch (Exception e) {
                        // 调用登陆失败的接口
                        try {
                            cdAuthenticationFailureHandler.onAuthenticationFailure(request,response,new UsernameNotFoundException("密码解密出错"));
                        } catch (ServletException servletException) {
                            servletException.printStackTrace();
                        }
                        throw new UsernameNotFoundException("密码解密出错");
                    }

                    localUser.setPassword(password);
                }
                if (map.get("mobile")!=null) {
                    String mobile = map.get("mobile");
                    localUser.setMobile(mobile);
                }
                if (map.get("email")!=null) {
                    String email = map.get("email");
                    localUser.setEmail(email);
                }

                SecurityThreadLocal.getInstance().set(localUser);
                SecurityThreadLocal.getValidateCodeThreadLocal().set(codeInRequest);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return codeInRequest;
    }

    private static ByteArrayOutputStream cloneInputStream(InputStream input) {
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int len;
            while ((len = input.read(buffer)) > -1) {
                baos.write(buffer, 0, len);
            }
            baos.flush();
            return baos;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
}
